Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

signer: create workaround for SignOutputRaw quirk #203

Merged
merged 1 commit into from
Nov 26, 2024

Conversation

guggero
Copy link
Member

@guggero guggero commented Nov 22, 2024

This commit fixes a long-standing issue with how SignOutputRaw populates the key descriptor before calling into lnd.
If the public key is available, only the public key is populated and the key locator (index+family) is not. That works well for any keys the wallet is aware of.
But if a wallet is restored from seed, it will not know any addresses/keys apart from index 0 of each family/account. And a lookup by public key only will fail.
To fix that, we add a new method SignOutputRawKeyLocator that has an updated behavior that also sends along the key locator if we're certain it is fully known.

Because changing any behavior in this area of the code might lead to breaking existing behavior some clients like Loop or Pool rely on, we explicitly don't change the original method but rather add a new one.

Pull Request Checklist

  • PR is opened against correct version branch.
  • Version compatibility matrix in the README and minimal required version
    in lnd_services.go are updated.
  • Update macaroon_recipes.go if your PR adds a new method that is called
    differently than the RPC method it invokes.

This commit fixes a long-standing issue with how SignOutputRaw populates
the key descriptor before calling into lnd.
If the public key is available, _only_ the public key is populated and
the key locator (index+family) is not. That works well for any keys the
wallet is aware of.
But if a wallet is restored from seed, it will not know any
addresses/keys apart from index 0 of each family/account. And a lookup
by public key only will fail.
To fix that, we add a new method SignOutputRawKeyLocator that has an
updated behavior that also sends along the key locator if we're certain
it is fully known.

Because changing any behavior in this area of the code might lead to
breaking existing behavior some clients like Loop or Pool rely on, we
explicitly don't change the original method but rather add a new one.
@guggero guggero requested a review from bhandras November 22, 2024 12:46
@guggero
Copy link
Member Author

guggero commented Nov 22, 2024

Will help solve the problem described in lightninglabs/taproot-assets#1208.

Copy link
Member

@bhandras bhandras left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🎉

// index will work. But for a freshly initialized wallet (e.g. restored
// from seed), we won't know any indexes greater than 0, so we _need_ to
// also specify the key locator and not just the public key.
fullDescriptor := func(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: instead of these two closures, we could just have one: descriptor(d keychain.KeyDescriptor, partial bool) and check for partial when filling the keydescriptor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to make this as clear and obvious as possible, with as much comment on why we're doing it that way, so I went with two closures on purpose. Just so nobody else spends debug hours on this (because this is already the second or third time I ran into this, once in Loop and once in Pool).

@jharveyb jharveyb self-requested a review November 22, 2024 17:39
Copy link
Contributor

@jharveyb jharveyb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🎉

@Roasbeef
Copy link
Member

But if a wallet is restored from seed, it will not know any addresses/keys apart from index 0 of each family/account. And a lookup by public key only will fail.

Correct that the public key look up will fail, however this should be accounted for with the existing logic: https://github.com/lightningnetwork/lnd/blob/94f7ed46deab346cc19ff1e6aa00cecdfe3eb775/keychain/btcwallet.go#L324-L363

This path is taken when we need to derive the private key based on the keyloc or keydesc.

Checking out the diff now.

// derived, we know that only using the public key for that very first
// index will work. But for a freshly initialized wallet (e.g. restored
// from seed), we won't know any indexes greater than 0, so we _need_ to
// also specify the key locator and not just the public key.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where I'm confused. If we only specify the public key, then we'll derive all private keys from 0 to 10k for that key family, checking the public key each time.

With the way the logic works, there're 3 main paths:

  1. You specify no public key, it checks the cache: https://github.com/lightningnetwork/lnd/blob/94f7ed46deab346cc19ff1e6aa00cecdfe3eb775/keychain/btcwallet.go#L266-L280
  2. Index is set, or no public key, it tries to derive from scratch: https://github.com/lightningnetwork/lnd/blob/94f7ed46deab346cc19ff1e6aa00cecdfe3eb775/keychain/btcwallet.go#L300-L322
  3. Only pubkey, it scans: https://github.com/lightningnetwork/lnd/blob/94f7ed46deab346cc19ff1e6aa00cecdfe3eb775/keychain/btcwallet.go#L324-L363

When we hit disk it eventually calls into deriveKeyFromPath: https://github.com/btcsuite/btcwallet/blob/66a3aeef6e78751e644a3d03fd856e982e0db4ac/waddrmgr/scoped_manager.go#L742-L769. DIsk wise, this only need the account to exist, as then it'll decrypt the master key and derive the addr as normal: https://github.com/btcsuite/btcwallet/blob/66a3aeef6e78751e644a3d03fd856e982e0db4ac/waddrmgr/scoped_manager.go#L385-L416

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't land in any of the code paths you linked... The problem is that the signrpc.SignOutputRaw RPC uses input.Signer.SignOutputRaw which is implemented by BtcWallet.SignOutputRaw which then calls into BtcWallet.fetchPrivKey which has a completely different logic.

// index will work. But for a freshly initialized wallet (e.g. restored
// from seed), we won't know any indexes greater than 0, so we _need_ to
// also specify the key locator and not just the public key.
fullDescriptor := func(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is resolved by setting both? From my reading of the code, only one or the other is read.

@dstadulis dstadulis added this to the tapd-v0.4.2 milestone Nov 23, 2024
@guggero guggero merged commit 4602d41 into lnd-18-4 Nov 26, 2024
1 check passed
@guggero guggero deleted the sign-output-raw-fix branch November 26, 2024 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

5 participants